package ace.oauth2.web.biz;

import ace.fw.exception.BusinessException;
import ace.fw.model.R;
import ace.fw.util.AceLocalDateTimeUtils;
import ace.oauth2.provider.base.support.AceUserDetailsService;
import ace.oauth2.provider.common.properties.OAuth2Property;
import ace.oauth2.enums.OAuth2BusinessErrorEnum;
import ace.oauth2.model.dto.OAuth2TokenDto;
import ace.oauth2.model.request.*;
import ace.oauth2.model.response.OAuth2CheckAccessTokenResponse;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

/**
 * @author Caspar
 * @contract 279397942@qq.com
 * @create 2020/11/1
 * @description
 */
@Slf4j
@RestController
@Component
public class OAuth2Biz {

    @Autowired
    private TokenEndpoint tokenEndpoint;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private CheckTokenEndpoint checkTokenEndpoint;
    @Autowired
    private OAuth2Property oAuth2Property;
    @Autowired
    private AceUserDetailsService userDetailsService;

    @SneakyThrows
    public R<OAuth2TokenDto> createToken(@Valid OAuth2CreateTokenRequest request) {

        String innerUserName = this.getInnerUserName(request.getTenantId(), request.getAccountId());

        return this.createTokenOrRefreshToken(() -> new HashMap<>(5) {
            {
                put("grant_type", "password");
                put("client_id", oAuth2Property.getDefaultClientProperty().getClientId());
                put("client_secret", oAuth2Property.getDefaultClientProperty().getClientSecret());
                put("username", innerUserName);
                put("password", request.getAccountId());
                put("accountId", request.getAccountId());
                put("tenantId", request.getTenantId());
                put("scope", "all");
            }
        });
    }

    @SneakyThrows
    public R<OAuth2TokenDto> refreshToken(@Valid OAuth2RefreshTokenRequest request) {
        return this.createTokenOrRefreshToken(() -> new HashMap<>(5) {
            {
                put("grant_type", "refresh_token");
                put("client_id", oAuth2Property.getDefaultClientProperty().getClientId());
                put("client_secret", oAuth2Property.getDefaultClientProperty().getClientSecret());
                put("refresh_token", request.getAccessToken());
                put("scope", "all");
            }
        });
    }

    /**
     * createToken and refreshToken method
     *
     * @param parametersSupplier
     * @return
     */
    @SneakyThrows
    private R<OAuth2TokenDto> createTokenOrRefreshToken(Supplier<Map<String, String>> parametersSupplier) {
        User user = new User(oAuth2Property.getDefaultClientProperty().getClientId(), oAuth2Property.getDefaultClientProperty().getClientSecret(), CollectionUtils.EMPTY_COLLECTION);

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, CollectionUtils.EMPTY_COLLECTION);

        Map<String, String> parameters = parametersSupplier.get();

        try {
            ResponseEntity<OAuth2AccessToken> responseEntity = tokenEndpoint.postAccessToken(authenticationToken, parameters);
            OAuth2TokenDto oAuth2TokenDto = OAuth2TokenDto.builder()
                    .accessToken(responseEntity.getBody().getValue())
                    .expiresIn(responseEntity.getBody().getExpiresIn())
                    .refreshToken(responseEntity.getBody().getRefreshToken().getValue())
                    .scope(new ArrayList<>(responseEntity.getBody().getScope()))
                    .tokenType(responseEntity.getBody().getTokenType())
                    .build();
            return R.ok(oAuth2TokenDto);
        } catch (Exception e) {
            if (e instanceof InsufficientAuthenticationException ||
                    e instanceof InvalidClientException ||
                    e instanceof InvalidRequestException ||
                    e instanceof InvalidGrantException ||
                    e instanceof InvalidScopeException ||
                    e instanceof UnsupportedGrantTypeException ||
                    e instanceof AuthenticationException
            ) {
                throw new BusinessException(e.getMessage());
            } else {
                throw e;
            }
        }
    }


    public R<Boolean> removeToken(@Valid OAuth2RemoveTokenRequest request) {
        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(request.getAccessToken());
        tokenStore.removeAccessToken(oAuth2AccessToken);
        tokenStore.removeRefreshToken(oAuth2AccessToken.getRefreshToken());
        return R.ok(Boolean.TRUE);
    }

    /**
     * 封装 {@link CheckTokenEndpoint#checkToken(String)}
     *
     * @param request
     * @return
     */

    public R<OAuth2CheckAccessTokenResponse> checkAccessToken(@Valid OAuth2CheckAccessTokenRequest request) {
        try {
            Map<String, ?> resultMap = checkTokenEndpoint.checkToken(request.getAccessToken());
            Long exp = (Long) resultMap.get("exp") * 1000;
            Long expireInSeconds = Duration.between(LocalDateTime.now(), AceLocalDateTimeUtils.fromTimestamp(exp)).toSeconds();
            OAuth2CheckAccessTokenResponse oAuth2CheckAccessTokenResponse = OAuth2CheckAccessTokenResponse.builder()
                    .active((Boolean) resultMap.get("active"))
                    .clientId((String) resultMap.get("client_id"))
                    .expireInSeconds(expireInSeconds)
                    .scope(new ArrayList<>((Collection<String>) resultMap.get("scope")))
                    .userName((String) resultMap.get("user_name"))
                    .accountId((String) resultMap.get("accountId"))
                    .tenantId((String) resultMap.get("tenantId"))
                    .build();

            return R.ok(oAuth2CheckAccessTokenResponse);
        } catch (Exception e) {
            if (e instanceof InvalidTokenException ||
                    e instanceof InsufficientAuthenticationException ||
                    e instanceof InvalidClientException ||
                    e instanceof InvalidRequestException ||
                    e instanceof InvalidGrantException ||
                    e instanceof InvalidScopeException ||
                    e instanceof UnsupportedGrantTypeException ||
                    e instanceof AuthenticationException
            ) {
                throw new BusinessException(OAuth2BusinessErrorEnum.TOKEN_INVALID.getCode(), e.getMessage());
            } else {
                log.error("[OAuth2Controller][checkAccessToken]异常", e);
                throw e;
            }
        }
    }


    public R<Boolean> removeAllTokenByAccountId(@Valid OAuth2RemoveAllTokenByAccountIdRequest request) {
        String innerUserName = this.getInnerUserName(request.getTenantId(), request.getAccountId());
        Collection<OAuth2AccessToken> oAuth2AccessTokens = tokenStore.findTokensByClientIdAndUserName(
                oAuth2Property.getDefaultClientProperty().getClientId(),
                innerUserName
        );
        if (CollectionUtils.isEmpty(oAuth2AccessTokens)) {
            return R.ok(Boolean.TRUE);
        }
        try {
            for (OAuth2AccessToken oAuth2AccessToken : oAuth2AccessTokens) {
                tokenStore.removeAccessToken(oAuth2AccessToken);
            }
            return R.ok(Boolean.TRUE);
        } catch (Exception ex) {
            log.error("[OAuth2Controller][removeAllTokenByAccountId]异常", ex);
            throw ex;
        }
    }

    private String getInnerUserName(String tenantId, String accountId) {
        return userDetailsService.buildOAuth2UserName(tenantId, accountId);
    }
}

